package com.andrewchatham.pony;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashSet;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Environment;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.RemoteViews;

import com.andrewchatham.SynctasticProto.Playlist;
import com.andrewchatham.SynctasticProto.Track;
import com.google.protobuf.InvalidProtocolBufferException;

/**
 * Keeps track of files we want to keep, and then scans the filesystem for
 * everything else so we can delete it.
 * 
 * Call touchFile() on every file we want to keep, and then findFilesToRemove()
 * will walk the directory given in the constructor and return everything not
 * touched.
 * 
 * @author chatham
 * 
 */
class FileManager {
  private File mDir;
  // We store the hash of files we want to keep, rather than the full filename,
  // to save memory.
  private HashSet<Integer> mNewFiles;

  public FileManager(File dir) {
    assert dir.isDirectory();
    mDir = dir;
    mNewFiles = new HashSet<Integer>();
  }

  /**
   * Touch the given file, so it won't be returned in findFilesToRemove().
   * 
   * @param file
   */
  public void touchFile(File file) {
    mNewFiles.add(file.getPath().hashCode());
  }

  /**
   * Return a list of all files in mDir which didn't have touchFile() called on
   * them.
   * 
   * @return
   */
  public ArrayList<File> findFilesToRemove() {
    ArrayList<File> toRemove = new ArrayList<File>();
    addFilesToRemove(mDir, toRemove);
    return toRemove;
  }

  /**
   * Recursively walk through dir, adding files which weren't touched to
   * toRemove.
   * 
   * @param dir
   * @param toRemove
   */
  private void addFilesToRemove(File dir, ArrayList<File> toRemove) {
    for (File f : dir.listFiles()) {
      if (f.isDirectory()) {
        addFilesToRemove(f, toRemove);
      } else {
        String path = f.getPath();
        if (!mNewFiles.contains(path.hashCode())) {
          toRemove.add(f);
        }
      }
    }
  }
}

public class SyncService extends Service implements
OnSharedPreferenceChangeListener, ErrorHandler {
  public SyncService() {
    Log.d(TAG, "Create sync");
  }

  static String TAG = "SyncService";

  public final static String ACTION_SYNC = "SYNC";

  // Debugging parameters.
  private final static boolean mFetchFiles = true;

  private PlaylistDB mPlaylistDB;
  private File mDir;
  private File mPlaylistDir;
  private Server mServer;
  private NotificationManager mNotifications;
  private Notification mSyncNotification;
  private WifiManager mWifi;
  private WifiManager.WifiLock mWifiLock;
  private PowerManager.WakeLock mWakeLock;
  private String mSSID;
  private FileManager mFileManager;
  private boolean mCancel;
  private boolean mSyncing;

  // Used to throttle progress updates.
  private int mLastMax;
  private int mLastProgress;

  private MediaScannerClient mScanner;

  private final static int NOTIFY_SYNC = 1;
  private final static int NOTIFY_ERROR = 1;

  private final ISyncService.Stub mBinder = new ISyncService.Stub() {
    public void startSync() {
      startSyncThread();
    }
    public void stopSync() {
      cancelSync();
    }
  };

  public IBinder onBind(Intent intent) {
    return mBinder;
  }

  @Override
  public void onCreate() {
    super.onCreate();

    SharedPreferences prefs = PreferenceManager
    .getDefaultSharedPreferences(this);
    ConnectivityManager connectivity = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    mWifi = (WifiManager) getSystemService(Context.WIFI_SERVICE);
    mWifiLock = mWifi.createWifiLock(TAG);

    PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

    mServer = new Server(this, this, connectivity
        .getNetworkInfo(ConnectivityManager.TYPE_WIFI), prefs);

    mNotifications = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

    setPreferences(prefs);
    prefs.registerOnSharedPreferenceChangeListener(this);

    mPlaylistDB = new PlaylistDB(this);
    mPlaylistDB.open();

    if (!Environment.getExternalStorageState()
        .equals(Environment.MEDIA_MOUNTED)) {
      Log.e(TAG, "Not mounted properly "
          + Environment.getExternalStorageState());
      return;
    }
    mDir = new File(Environment.getExternalStorageDirectory(), "SynctasticPony");
    mDir.mkdirs();
    mPlaylistDir = new File(mDir, "playlists");
    mPlaylistDir.mkdir();
    mFileManager = new FileManager(mDir);
    mSyncing = false;
  }

  /**
   * Attempt to cancel the sync. isCanceled() will return true.
   */
  private synchronized void cancelSync() {
    mCancel = true;
  }

  /**
   * Check whether we should quit the sync.
   * @return
   */
  private synchronized boolean isCanceled() {
    return mCancel;
  }

  private void startSyncThread() {
    new Thread() {
      public void run() {
        handleSyncIntent();
      }
    }.start();
  }

  /**
   * Set mSyncing to value and return the old value
   * @param value new value of mSyncing
   * @return 
   */
  private synchronized boolean setSyncing(boolean value) {
    boolean old = mSyncing;
    mSyncing = value;
    return old;
  }

  private void Sync() {
    mCancel = false;
    if (setSyncing(true))
      return;  // already syncing
    mScanner = new MediaScannerClient(this);
    mWifiLock.acquire(); // keep wifi awake for the duration
    mWakeLock.acquire(); // keep cpu on but not necesssarily the screen

    try {
      createSyncNotification();
      ArrayList<String> playlists = mPlaylistDB.getSyncPlaylists();
      for (String playlist : playlists) {
        Log.d(TAG, "Syncing " + playlist);
        if (isCanceled())
          return;
        SyncPlaylist(playlist);
      }

      if (isCanceled())
        return;

      updateSyncNotificationText("Deleting old media", "");
      DeleteOldFiles();

    } finally {
      mWifiLock.release();
      mWakeLock.release();
      mNotifications.cancel(NOTIFY_SYNC);
      mSyncNotification = null;
      mScanner.setDone();
      mScanner.waitUntilComplete();
      mScanner = null;
      setSyncing(false);
    }
  }

  /**
   * Sync a playlist. Requests the list of tracks for that playlist and calls
   * SyncTrack() on each of them. Finally creates an M3U playlist file with
   * those tracks.
   * 
   * @param playlist
   */
  private void SyncPlaylist(String playlist_name) {
    if (isCanceled())
      return;

    HTTPRequest req = new HTTPRequest(mServer.formatRequest("/playlist",
        "playlist=" + URLEncoder.encode(playlist_name)));
    req.run();
    Playlist playlist;
    try {
      playlist = Playlist.parseFrom(req.response());
    } catch (InvalidProtocolBufferException e1) {
      Log.e(TAG, e1.toString());
      return;
    }
    int count = 0;
    ArrayList<String> paths = new ArrayList<String>();
    for (Track track : playlist.getTrackList()) {
      if (isCanceled())
        return;

      String path = track.getRemotePath();
      Log.d(TAG, String.format("Found %s from %s", playlist_name, path));
      String[] pieces = path.split("[/\\\\]"); // find the basename
      String shortPath = pieces[pieces.length - 1];
      if (shortPath.length() > 30)
        shortPath = shortPath.substring(0, 30);
      updateSyncNotificationText("Syncing " + playlist.getName(), shortPath);
      SyncTrack(path);
      updateSyncProgress(R.id.sync_notify_progress, playlist.getTrackCount(),
          count++);
      paths.add(path);
    }
    MediaUtil.writePlaylist(this, playlist_name, paths);
  }

  /**
   * Return the local path where the given remote file should be copied.
   * 
   * @param remote_path
   *          Path of the media file on the remote server.
   * @return Path to which the file should be copied on the phone
   */
  private File LocalPath(String remote_path) {
    String path = String.format("%s/%02d/%s", mDir, Math.abs(remote_path
        .hashCode()) % 100, remote_path.replace('/', '_'));
    return new File(path);
  }

  private void SyncTrack(String path) {
    File localPath = LocalPath(path);
    if (localPath.exists()) {
      Log.d(TAG, "Skpping " + path);
      mFileManager.touchFile(localPath);
      return;
    }

    localPath.getParentFile().mkdirs();
    try {
      Log.d(TAG, String.format("Fetching %s to %s", path, localPath.getPath()));
      if (mFetchFiles) {
        HTTPRequest req = new HTTPRequest(mServer.formatRequest("/file",
            "file=" + URLEncoder.encode(path)), new FileOutputStream(localPath));
        req.setProgressHandler(new HTTPRequest.ProgressHandler() {
          public void setProgress(int max, int progress) {
            updateSyncProgress(R.id.sync_notify_progress_secondary, max,
                progress);
          }
        });
        req.run();
      }
      mFileManager.touchFile(localPath);
      mScanner.scan(localPath.getPath());
    } catch (FileNotFoundException e) {
      localPath.delete();
      e.printStackTrace();
      return;
    }
  }

  private void handleSyncIntent() {
    if (mSSID.equals("")) {
      // TODO(chatham): Would be nice to send an intent to the preferences
      // activity so they can turn on wifi.
      notifyError("Select a network from settings");
      return;
    }
    if (!mWifi.isWifiEnabled()) {
      notifyError("Must enable wifi");
      return;
    }

    String ssid = mWifi.getConnectionInfo().getSSID();
    if (!mSSID.equals(ssid)) {
      Log.d(TAG, String.format("%s vs %s", mSSID, ssid));
      notifyError("Must be connected to network " + mSSID);
      return;
    }

    if (!mServer.waitForDiscovery()) {
      notifyError("Could not find server");
      return;
    }

    Sync();
  }

  /**
   * Delete any files which weren't touched in the file manager, as these are no
   * longer needed.
   */
  private void DeleteOldFiles() {
    ArrayList<File> toRemove = mFileManager.findFilesToRemove();
    int count = 0;
    for (File f : toRemove) {
      RemoveOldFile(f);
      updateSyncProgress(R.id.sync_notify_progress, toRemove.size(), ++count);
    }
  }

  /**
   * Delete the file from local storage
   * 
   * @param localPath
   */
  private void RemoveOldFile(File localPath) {
    if (localPath.delete()) {
      String path = localPath.getPath();
      MediaUtil.deleteTrack(this, path);
      Log.d(TAG, "Deleted " + path);
    }
  }

  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    setPreferences(prefs);
  }

  private void setPreferences(SharedPreferences prefs) {
    mSSID = prefs.getString(getString(R.string.ssid_key), "");
  }

  public void ShowError(String s) {
    notifyError(s);
    Log.e(TAG, s);
  }

  public void ShowError(int id) {
    ShowError(getString(id));
  }

  private void notifyError(String text) {
    Notification n = new Notification(android.R.drawable.ic_dialog_alert,
        "Sync", System.currentTimeMillis());
    updateNotification(NOTIFY_ERROR, n, text);
  }

  private void updateNotification(int notifyId, Notification n, String text) {
    Intent intent = new Intent(this, SynctasticPony.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0);
    n.setLatestEventInfo(this, "Syncing", text, contentIntent);
    mNotifications.notify(notifyId, n);
  }

  private void createSyncNotification() {
    mSyncNotification = new Notification(android.R.drawable.arrow_up_float,
        "Sync", System.currentTimeMillis());
    RemoteViews contentView = new RemoteViews(getPackageName(),
        R.layout.sync_notification);
    contentView.setTextViewText(R.id.sync_notify_text, "Syncing");
    mSyncNotification.contentView = contentView;
  }

  private void updateSyncNotificationText(String text, String secondary) {
    RemoteViews views = mSyncNotification.contentView;
    views.setTextViewText(R.id.sync_notify_text, text);
    views.setTextViewText(R.id.sync_notify_secondary_text, secondary);
    sendSyncNotification();
  }

  private void updateSyncProgress(int id, int max, int progress) {
    // If this is the first progress update, we hit 100%, or changed more than
    // 10%, do an update. This may get called very frequently, and we want to
    // throttle updates.
    if (max != mLastMax || max == progress
        || 100 * (progress - mLastProgress) / max > 10) {
      mSyncNotification.contentView.setProgressBar(id, max, progress, false);
      sendSyncNotification();
      if (max == progress) {
        // If we just finished, reset things.
        mLastMax = 0;
        mLastProgress = 0;
      } else {
        mLastMax = max;
        mLastProgress = progress;
      }
    }
  }

  private void sendSyncNotification() {
    Intent intent = new Intent(this, SynctasticPony.class);
    mSyncNotification.contentIntent = PendingIntent.getActivity(this, 0,
        intent, 0);
    mNotifications.notify(NOTIFY_SYNC, mSyncNotification);
  }
}
